package configuration;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class Configuration {
	//OVERVIEW: This class provides a configuration backed for storing usernames, passwords, configuration info, etc
	//Also provides a writer so configuration can be saved and reloaded
	//REP(c) = file !=null && domDoc != null && db !=null and has been initialized
	//AF(c) = DB maps to a form of a Hash Tree
	private String file;
	private Document domDoc;
	private final ConcurrentHashMap<String, ModuleEntry> db;

	public Configuration(final String fn) {
		db = new ConcurrentHashMap<String, ModuleEntry>();
		try {
			//Use java built in XML parsing stuff for our advantage
			file = fn;
			final DocumentBuilderFactory theFac = DocumentBuilderFactory
					.newInstance();
			final DocumentBuilder docComp = theFac.newDocumentBuilder();
			domDoc = docComp.parse(new File(fn));
			domDoc.getDocumentElement().normalize();
			//We Now have loaded the XML file into a traversable DOM format
			NodeList temp = domDoc.getChildNodes();
			//We Must have an AIMCONFIG, it holds the configuration for the program
			if (temp.item(0).getNodeName().equals("AIMCONFIG")) {
				temp = domDoc.getElementsByTagName("Module");
				//Get some variables ready to hold data
				Element mods;
				NodeList temp1;
				NodeList temp0;
				Element idents;
				NodeList tagEl;
				String tag = "";
				NodeList valueL;
				String value = "";
				String nm;
				ModuleEntry tempMod;
				DBEntry tempEntry;
				//Alright, start at level of modules and get all of them
				for (int i = 0; i < temp.getLength(); i++) {
					mods = (Element) temp.item(i);
					temp0 = mods.getChildNodes();
					nm = temp0.item(0).getNodeValue().trim();
					if (db.containsKey(nm)) {
						System.err.println("Module Already Exists");
					} else {
						//Now that we have a list of all the modules, get their identifiers
						tempMod = new ModuleEntry(nm);
						
						temp1 = mods.getElementsByTagName("Identifier");
						for (int j = 0; j < temp1.getLength(); j++) {
							idents = (Element) temp1.item(j);
							final String identifier = idents.getChildNodes()
									.item(0).getNodeValue().trim();
							tempEntry = new DBEntry(identifier);
							//Now get the Tags for each identifier
							tagEl = idents.getElementsByTagName("Tag");
							if (tagEl == null) {
								// We Need a bad file exception
							} else {
								for (int k = 0; k < tagEl.getLength(); k++) {
									idents = (Element) tagEl.item(k);
									//Get Values
									tag = idents.getChildNodes().item(0)
											.getNodeValue();
									valueL = idents
											.getElementsByTagName("Value");
									final Element valueLE = (Element) valueL
											.item(0);
									value = valueLE.getChildNodes().item(0)
											.getNodeValue().trim();
									try {
										//Make tag value combos for insertion
										tempEntry.add(tag, value);
									} catch (final EntryExists e) {
										System.err.println("Already in DB");
									}
									try {
										//Insert tag value combos into a module
										tempMod.add(tempEntry);
									} catch (final EntryExists e) {
										// System.err.println("Already in DB");
									}
								}
							}
						}
						//Put modules in tree
						db.put(nm, tempMod);
					}
				}
			}
		} catch (final ParserConfigurationException e) {
		} catch (final IOException e) {
			System.out.println("FileNotFound");
		} catch (final SAXException e) {
		}
	}

	public void addEntry(final String module, final String id,
			final String tag, final String value) throws EntryExists {
	    //* Requires: true
	    //* Modifies: this
	    //* Effects: If the entry described exists, throws EntryExistsException, otherwise adds the described entry.
		if (db.containsKey(module)) {
			//If the module already exists we can go ahead and add entry
			db.get(module).addLeaf(id, tag, value);
		} else {
			//If the module doesn't exist, we must make it first
			final ModuleEntry temp = new ModuleEntry(module);
			final DBEntry tempa = new DBEntry(id);
			tempa.add(tag, value);
			temp.add(tempa);
			db.put(module, temp);
		}
	}

	public String getEntry(final String mod, final String id, final String tag)
			throws NoEntryException {
	    //* Requires: true
	    //* Modifies: none
	    //* Effects: Returns the associated value if there exists an entry with a corresponding module, identifier, and tag, otherwise throws NoEntryException


		if (this.isEntry(mod, id, tag)) {
			//Hand off getting of value down the ladder
			final ModuleEntry temp = db.get(mod);
			return temp.getEntry(id, tag);
		} else {
			throw new NoEntryException();
		}
	}

	public boolean isEntry(final String mod, final String id) {
	    //* Requires: true
	    //* Modifies: none
	    //* Effects: Returns true if there exists an entry with a corresponding module and identifier, otherwise returns false
		if (db.containsKey(mod)) {
			final ModuleEntry temp = db.get(mod);
			return temp.isEntry(id);
		} else {
			return false;
		}
	}

	public boolean isEntry(final String mod, final String id, final String tag) {
	    //* Requires: true
	    //* Modifies: none
	    //* Effects: Returns true if there exists an entry with a corresponding module, identifier and tag, otherwise returns false
		if (db.containsKey(mod)) {
			final ModuleEntry temp = db.get(mod);
			return temp.isEntry(id, tag);
		} else {
			return false;
		}
	}

	public void removeEntry(final String module, final String id)
			throws NoEntryException {
	    //* Requires: true
	   // * Modifies: this
	    //* Effects: Removes all the entries corresponding to the module, id. Throws NoEntryException if no such entry exists.
		if (this.isEntry(module, id)) {
			db.get(module).removeID(id);
		} else {
			throw new NoEntryException();
		}
	}

	public void addModule(final String Module) {
	    //* Requires: true
	    //* Modifies: this
	    //* Effects: If the entry described exists, throws EntryExistsException, otherwise adds the described entry.
		if (!db.containsKey(Module)) {
			final ModuleEntry temp = new ModuleEntry(Module);
			db.put(Module, temp);
		}
	}

	public void removeEntry(final String module, final String id,
			final String tag) throws NoEntryException {
	    //* Requires: true
	    //* Modifies: this
	    //* Effects: Removes all the entries corresponding to the module, id. Throws NoEntryException if no such entry exists.
		if (this.isEntry(module, id)) {
			db.get(module).removeTag(id, tag);
		} else {
			throw new NoEntryException();
		}
	}

	public Set<String> getTags(final String Module, final String ID)
			throws NoEntryException {
	   // * Requires: true
	    //* Modifies: this
	    //* Effects: Returns a Set of the tags associated with that identifier, otherwise throws NoEntryException.
		if (this.isEntry(Module, ID)) {
			final ModuleEntry temp = db.get(Module);
			return temp.getTags(ID);
		} else {
			throw new NoEntryException();
		}
	}

	public boolean isModule(final String Module) {
	    //* Requires: true
	    //* Modifies: none
	    //* Effects: Returns true if there exists a corresponding module, otherwise returns false
		return db.containsKey(Module);
	}

	public void changeEntry(final String Module, final String ID,
			final String tag, final String value) throws NoEntryException {
		//* Requires: true
	    //* Modifies: this
		//* Effects: Changes specified entry to the specified value, otherwise throw NoEntryException
		if (db.containsKey(Module)) {
			db.get(Module).changeEntry(ID, tag, value);
		} else {
			throw new NoEntryException();
		}
	}
	public Set<String> getModules() {
		//* Requires: true
	    //* Modifies: none
		//* Effects: Returns set of modules
		return db.keySet();
	}
	public void addID(String Module, String ID) throws EntryExists {
		if (db.containsKey(Module)) {
			//If the module already exists we can go ahead and add entry
			db.get(Module).addID(ID);
		} else {
			//If the module doesn't exist, we must make it first
			final ModuleEntry temp = new ModuleEntry(Module);
			temp.addID(ID);
			db.put(Module, temp);
		}
	}
	public Set<String> getEntries(String Module) throws NoEntryException {
		//* Requires: true
	    //* Modifies: none
		//* Effects: Returns set of IDs associated with module, otherwise throw NoEntryException
	if(db.containsKey(Module)) {
		return db.get(Module).getIDs();
	}
	else {
		throw new NoEntryException();
	}
	
	}

	public boolean writeFile() {
		//* Requires: true
	    //* Modifies: none
	    //* Effects: Writes current configuration values back to original file and returns true, otherwise returns false
		try {
			//Create a File writer and buffer it
			final FileWriter stalin = new FileWriter(file);
			final BufferedWriter notebook = new BufferedWriter(stalin);
			//We need an AIMCONFIG
			notebook.write("<AIMCONFIG>");
			notebook.newLine();
			// Copy table so to remove race conditions(ConcurrentHashMap does
			// not guarentee order among iterators)
			final HashMap<String, ModuleEntry> temp = new HashMap<String, ModuleEntry>(
					db);
			for (final String key : temp.keySet()) {
				notebook.write("<Module>");
				notebook.write(key);
				//Have the appropriate division of the Tree write its values
				temp.get(key).write(notebook);
				notebook.write("</Module>");
				notebook.newLine();
			}
			notebook.write("</AIMCONFIG>");
			notebook.close();
			return true;
		} catch (final IOException e) {
			return false;
		}
	}
}